package com.yk.auth.service;

import cn.dev33.satoken.exception.NotLoginException;
import cn.dev33.satoken.secure.BCrypt;
import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.HttpUtil;
import cn.hutool.http.useragent.UserAgent;
import cn.hutool.http.useragent.UserAgentUtil;
import cn.hutool.json.JSONObject;
import com.yk.api.system.dto.RegisterUserDTO;
import com.yk.api.system.dto.UpdateUserDTO;
import com.yk.api.system.dto.UserWxDTO;
import com.yk.api.system.model.UserFeignService;
import com.yk.api.system.model.UserWxFeignService;
import com.yk.auth.param.*;
import com.yk.auth.properties.UserPasswordProperties;
import com.yk.common.core.constant.CacheConstants;
import com.yk.common.core.constant.Constants;
import com.yk.common.core.constant.NumberConstant;
import com.yk.common.core.domain.LoginUser;
import com.yk.common.core.domain.Result;
import com.yk.common.core.dto.SendWechatDTO;
import com.yk.common.core.enums.DeviceType;
import com.yk.common.core.exception.ServiceException;
import com.yk.common.core.exception.user.UserException;
import com.yk.common.core.utils.*;
import com.yk.common.core.utils.ip.AddressUtils;
import com.yk.common.log.domain.LogininforEvent;
import com.yk.common.redis.service.RedisService;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

import javax.servlet.http.HttpServletRequest;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;

/**
 * 登录校验方法
 *
 * @author lmx
 * @date 2023/10/13 11:23
 */
@Service
@RequiredArgsConstructor
public class SysLoginService {

    private final UserFeignService userFeignService;
    private final UserWxFeignService userWxFeignService;
    private final UserPasswordProperties userPasswordProperties;
    private final RedisService redisService;
    private final SendUtils sendUtils;

    @Value("${wx.xcx.app-id}")
    private String appId;
    @Value("${wx.xcx.app-secret}")
    private String appSecret;
    @Value("${wx.xcx.grant_type}")
    private String grantType;
    @Value("${wx.xcx.session_url}")
    private String sessionUrl;

    /**
     * 账号密码登录
     *
     * @param form 用户登录对象
     */
    public String login(LoginBody form, DeviceType deviceType) {
        String username = form.getUsername();
        String password = form.getPassword();
        Boolean autoLogin = form.getAutoLogin();
        Result<LoginUser> userResult = userFeignService.getUserInfo(username);
        if (Result.isError(userResult)) {
            throw new UserException(userResult.getMsg());
        }
        if (Result.isNull(userResult)) {
            recordLogininfor(username, Constants.LOGIN_FAIL, "登录用户不存在");
            throw new UserException("登录用户：" + username + " 不存在");
        }
        LoginUser userInfo = userResult.getData();
        // 登录校验
        checkLogin(username, () -> !BCrypt.checkpw(password, userInfo.getPassword()));
        // 获取登录token
        if (Objects.nonNull(autoLogin) && autoLogin) {
            LoginHelper.login2TimeOutByDevice(userInfo, deviceType);
        } else {
            LoginHelper.loginByDevice(userInfo, deviceType);
        }
        // 登录日志
        recordLogininfor(username, Constants.LOGIN_SUCCESS, "登录成功");
        return StpUtil.getTokenValue();
    }

    /**
     * 短信登录
     *
     * @param smsLoginBody 短信登录对象
     */
    public String smsLogin(SmsLoginBody smsLoginBody, DeviceType deviceType) {
        String phone = smsLoginBody.getPhone();
        String smsCode = smsLoginBody.getSmsCode();
        Boolean autoLogin = smsLoginBody.getAutoLogin();
        Result<LoginUser> userResult = userFeignService.getUserInfoByPhone(phone);
        if (Result.isError(userResult)) {
            throw new UserException(userResult.getMsg());
        }

        if (Result.isNull(userResult)) {
            recordLogininfor(phone, Constants.LOGIN_FAIL, "登录用户不存在");
            throw new UserException("登录用户：" + phone + " 不存在");
        }
        LoginUser userInfo = userResult.getData();
        if (DeviceType.XCX.getDevice().equals(deviceType.getDevice())){
            checkLogin(userInfo.getUsername(), () -> validateSmsCode(phone, smsCode, NumberConstant.SIX));
        }else {
            checkLogin(userInfo.getUsername(), () -> validateSmsCode(phone, smsCode, NumberConstant.ZERO));
        }
        // 生成token
        return geLoginToken(autoLogin, userInfo, deviceType);
    }

    /**
     * 微信公众号推送
     * @param gOpenId 公众号ID
     */
    private void sendGzhMessage(String gOpenId) {
        SendWechatDTO dto = new SendWechatDTO();
        dto.setType(NumberConstant.ONE_STR);
        HttpServletRequest request = ServletUtils.getRequest();
        String address;
        if (Objects.nonNull(request)) {
            String ip = ServletUtils.getClientIP(request);
            address = AddressUtils.getRealAddressByIP(ip);
        }else {
            address = "未知";
        }
        dto.setAddress(address);
        dto.setOpenId(gOpenId);
        sendUtils.sendWechatMessage(dto);
    }

    /**
     * 登录获取token
     *
     * @param autoLogin
     * @param userInfo
     * @return
     */
    private String geLoginToken(Boolean autoLogin, LoginUser userInfo, DeviceType deviceType) {
        // 生成token
        if (Objects.nonNull(autoLogin) && autoLogin) {
            LoginHelper.login2TimeOutByDevice(userInfo, deviceType);
        } else {
            LoginHelper.loginByDevice(userInfo, deviceType);
        }
        recordLogininfor(userInfo.getUsername(), Constants.LOGIN_SUCCESS, "登录成功");
        return StpUtil.getTokenValue();
    }

    public String loginAppKey(String jsCode) {
        JSONObject obj = getWxUserInfo(jsCode);
        String session_key = obj.getStr("session_key");
        String openId = obj.getStr("openid");
        if (StrUtil.isEmpty(session_key) || StrUtil.isEmpty(openId)) {
            throw new UserException("jsCode无效");
        }
        // openid如果存在则直接登录，否则将session_key返给小程序，
        Result<LoginUser> userResult = userFeignService.getUserInfoByOpenId(openId);
        if (Result.isError(userResult)) {
            throw new UserException(userResult.getMsg());
        }
        if (Result.isNull(userResult)) {
            return session_key;
        }
        LoginUser userInfo = userResult.getData();
        LoginHelper.login2TimeOutByDevice(userInfo, DeviceType.XCX);
        recordLogininfor(userInfo.getUsername(), Constants.LOGIN_SUCCESS, "登录成功");
        return StpUtil.getTokenValue();
    }

    /**
     * 小程序登录
     */
    public String xcxLogin(XcxLoginBody param) {
        String vi = param.getVi();
        String encryptedData = param.getEncryptedData();
        String jsCode = param.getJsCode();
        JSONObject obj = getWxUserInfo(jsCode);
        String sessionKey = obj.getStr("session_key");
        String openId = obj.getStr("openid");
        String unionId = obj.getStr("unionid");
        if (StrUtil.isEmpty(sessionKey) || StrUtil.isEmpty(openId)) {
            throw new UserException("jsCode无效");
        }
        LoginUser userInfo;
        // 解析手机号
        JSONObject jsonObject = WechatDecryptDataUtil.getPhoneNumber(sessionKey, encryptedData, vi);
        if (Objects.isNull(jsonObject)) {
            throw new UserException("登录失败");
        }
        String phone = jsonObject.getStr("phoneNumber");
        Result<LoginUser> userInfoByPhone = userFeignService.getUserInfoByPhone(phone);
        String errorMsg = "用户不存在";
        if (Result.isError(userInfoByPhone) && !errorMsg.equals(userInfoByPhone.getMsg())) {
            throw new UserException(userInfoByPhone.getMsg());
        }
        if (!Result.isNull(userInfoByPhone)) {
            userInfo = userInfoByPhone.getData();
        } else {
            // 注册
            Result<LoginUser> result = userFeignService.registerUserInfo(new RegisterUserDTO(phone, phone));
            if (Result.isError(result) || Result.isNull(result)) {
                throw new UserException("登录失败");
            }
            userInfo = result.getData();
        }
        // 关联表
        saveXcxUserWx(openId, unionId, userInfo.getUserId());
        // 生成token
        LoginHelper.loginByDevice(userInfo, DeviceType.XCX);
        recordLogininfor(userInfo.getUsername(), Constants.LOGIN_SUCCESS, "登录成功");
        return StpUtil.getTokenValue();
    }

    /**
     * 根据jscode获取信息
     *
     * @param jsCode 小程序code
     * @return 结果
     */
    public JSONObject getWxUserInfo(String jsCode) {
        Map<String, Object> params = MapUtil.newHashMap();
        params.put("appid", appId);
        params.put("secret", appSecret);
        params.put("js_code", jsCode);
        params.put("grant_type", grantType);
        String result = HttpUtil.get(sessionUrl, params);
        return new JSONObject(result);
    }

    /**
     * 微信小程序关联表保存
     */
    public void saveXcxUserWx(String openId, String unionId, Long userId) {
        UserWxDTO wxDTO = new UserWxDTO();
        wxDTO.setXOpenId(openId);
        wxDTO.setUnionId(unionId);
        wxDTO.setUserId(userId);
        Result<Boolean> booleanResult = userWxFeignService.saveUserWx(wxDTO);
        if (Result.isError(booleanResult)) {
            throw new UserException("登录失败");
        }
    }

    /**
     * 微信公众号关联表保存
     */
    public void saveGzhUserWx(String openId, Long userId) {
        UserWxDTO wxDTO = new UserWxDTO();
        wxDTO.setGOpenId(openId);
        wxDTO.setUserId(userId);
        Result<Boolean> booleanResult = userWxFeignService.saveUserWx(wxDTO);
        if (Result.isError(booleanResult)) {
            throw new UserException("登录失败");
        }
    }

    /**
     * 退出登录
     */
    public void logout() {
        try {
            LoginUser loginUser = LoginHelper.getLoginUser();
            recordLogininfor(loginUser.getUsername(), Constants.LOGOUT, "退出成功");
        } catch (NotLoginException ignored) {
            throw new ServiceException("退出失败");
        } finally {
            StpUtil.logout();
        }
    }

    /**
     * 用户注册
     *
     * @param registerBody 用户注册对象
     */
    public void register(RegisterBody registerBody) {
        String password = registerBody.getPassword();
        String confirmPassword = registerBody.getConfirmPassword();
        if (!StringUtils.equals(password, confirmPassword)) {
            throw new UserException("两次输入的密码不匹配");
        }
        String smsCode = registerBody.getSmsCode();
        String phone = registerBody.getPhone();
        if (StringUtils.isNotEmpty(smsCode) && validateSmsCode(phone, smsCode, NumberConstant.ONE)) {
            throw new UserException("验证码错误");
        }
        String username = registerBody.getUsername();
        // 注册用户信息
        RegisterUserDTO dto = new RegisterUserDTO();
        dto.setUserName(username);
        dto.setNickName(registerBody.getNickName());
        dto.setPassword(BCrypt.hashpw(password));
        dto.setPhone(registerBody.getPhone());
        Result<LoginUser> result = userFeignService.registerUserInfo(dto);
        if (Result.isError(result)) {
            throw new UserException(result.getMsg());
        }
        if (Result.isNull(result)) {
            throw new UserException("注册失败");
        }
        recordLogininfor(username, Constants.REGISTER, "注册成功");
    }

    /**
     * 记录登录信息
     *
     * @param username 用户名
     * @param status   状态
     * @param message  消息内容
     */
    public void recordLogininfor(String username, String status, String message) {
        HttpServletRequest request = ServletUtils.getRequest();
        if (Objects.isNull(request)) {
            return;
        }
        final UserAgent userAgent = UserAgentUtil.parse(request.getHeader("User-Agent"));
        final String ip = ServletUtils.getClientIP(request);
        String address = AddressUtils.getRealAddressByIP(ip);
        // 获取客户端操作系统
        String os = userAgent.getOs().getName();
        // 获取客户端浏览器
        String browser = userAgent.getBrowser().getName();
        // 封装对象
        LogininforEvent logininfor = new LogininforEvent();
        logininfor.setUserName(username);
        logininfor.setIpaddr(ip);
        logininfor.setLoginLocation(address);
        logininfor.setBrowser(browser);
        logininfor.setOs(os);
        logininfor.setMsg(message);
        // 日志状态
        if (StringUtils.equalsAny(status, Constants.LOGIN_SUCCESS, Constants.LOGOUT, Constants.REGISTER)) {
            logininfor.setStatus(Constants.LOGIN_SUCCESS_STATUS);
        } else if (Constants.LOGIN_FAIL.equals(status)) {
            logininfor.setStatus(Constants.LOGIN_FAIL_STATUS);
        }
        SpringUtils.context().publishEvent(logininfor);
    }

    /**
     * 校验短信验证码
     *
     * @param phone   手机号
     * @param smsCode 短信验证码
     * @param type    短信类型
     * @return true 成功 false 失败
     */
    public boolean validateSmsCode(String phone, String smsCode, Integer type) {
        String key = CaptchaCodeKeyUtils.getCaptchaKey(type);
        String code = redisService.getCacheObject(key + phone);
        if (StringUtils.isBlank(code)) {
            recordLogininfor(phone, Constants.LOGIN_FAIL, "验证码已过期");
            throw new ServiceException("验证码已过期");
        }
        return !code.equals(smsCode);
    }

    /**
     * 登录校验
     *
     * @param username 用户名
     * @param supplier 函数式接口
     */
    public void checkLogin(String username, Supplier<Boolean> supplier) {
        String errorKey = CacheConstants.PWD_ERR_CNT_KEY + username;
        String loginFail = Constants.LOGIN_FAIL;
        Integer maxRetryCount = userPasswordProperties.getMaxRetryCount();
        Integer lockTime = userPasswordProperties.getLockTime();
        // 获取用户登录错误次数
        Integer errorNumber = redisService.getCacheObject(errorKey);
        // 锁定时间内登录 则踢出
        if (ObjectUtil.isNotNull(errorNumber) && errorNumber.equals(maxRetryCount)) {
            recordLogininfor(username, loginFail, "登录重试已超出限制：" + maxRetryCount + "|" + "锁定登录时间：" + lockTime);
            throw new UserException("账户已锁定");
        }
        if (supplier.get()) {
            // 是否第一次
            errorNumber = ObjectUtil.isNull(errorNumber) ? 1 : errorNumber + 1;
            // 达到规定错误次数 则锁定登录
            if (errorNumber.equals(maxRetryCount)) {
                redisService.setCacheObject(errorKey, errorNumber, lockTime.longValue(), TimeUnit.MINUTES);
                recordLogininfor(username, loginFail, "登录重试已超出限制：" + maxRetryCount + "|" + "锁定登录时间：" + lockTime);
                throw new UserException("账户已锁定");
            } else {
                // 未达到规定错误次数 则递增
                redisService.setCacheObject(errorKey, errorNumber);
                recordLogininfor(username, loginFail, "登录重试次数：" + errorNumber);
                throw new UserException("账户或密码错误");
            }
        }
        // 登录成功 清空错误次数
        redisService.deleteObject(errorKey);
    }

    /**
     * 忘记密码
     *
     * @param forgotPasswordBody 用户忘记密码对象
     */
    public void forgotPassword(ForgotPasswordBody forgotPasswordBody) {
        String password = forgotPasswordBody.getPassword();
        String confirmPassword = forgotPasswordBody.getConfirmPassword();
        if (!StringUtils.equals(password, confirmPassword)) {
            throw new UserException("两次输入的密码不匹配");
        }
        String smsCode = forgotPasswordBody.getSmsCode();
        String phone = forgotPasswordBody.getPhone();
        if (StringUtils.isNotEmpty(smsCode) && validateSmsCode(phone, smsCode, forgotPasswordBody.getCodeType())) {
            throw new UserException("验证码错误");
        }
        UpdateUserDTO dto = new UpdateUserDTO();
        dto.setPassword(password);
        dto.setPhone(forgotPasswordBody.getPhone());
        Result<Boolean> result = userFeignService.forgotAndUpdatePassword(dto);
        if (Result.isError(result)) {
            throw new UserException(result.getMsg());
        }
    }

    /**
     * 修改手机号
     *
     * @param body 修改手机号对象
     */
    public void updatePhone(UpdatePhoneBody body) {
        String oldPhone = body.getOldPhone();
        String newPhone = body.getNewPhone();
        String smsCode = body.getSmsCode();
        if (StringUtils.equals(newPhone, oldPhone)) {
            throw new UserException("新旧手机号不能相同");
        }
        if (StringUtils.isNotEmpty(smsCode) && validateSmsCode(newPhone, smsCode, NumberConstant.THREE)) {
            throw new UserException("验证码错误");
        }
        UpdateUserDTO dto = new UpdateUserDTO();
        dto.setOldPhone(oldPhone);
        dto.setPhone(newPhone);
        Result<Boolean> result = userFeignService.updatePhone(dto);
        if (Result.isError(result)) {
            throw new UserException(result.getMsg());
        }
    }

    /**
     * 修改手机号
     *
     * @param body 修改手机号对象
     */
    public void updateAppPhone(UpdateAppPhoneBody body) {
        String newPhone = body.getNewPhone();
        String smsCode = body.getSmsCode();
        if (StringUtils.isNotEmpty(smsCode) && validateSmsCode(newPhone, smsCode, NumberConstant.THREE)) {
            throw new UserException("验证码错误");
        }
        LoginUser loginUser = LoginHelper.getLoginUser();
        if (StringUtils.equals(newPhone, loginUser.getPhone())) {
            throw new UserException("新旧手机号不能相同");
        }
        UpdateUserDTO dto = new UpdateUserDTO();
        dto.setOldPhone(loginUser.getPhone());
        dto.setPhone(newPhone);
        Result<Boolean> result = userFeignService.updatePhone(dto);
        if (Result.isError(result)) {
            throw new UserException(result.getMsg());
        }
    }
}
